﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

namespace PGMapper
{
    // exists so we can talk about MappedTypes without knowing the type.
    interface IMappedType
    {
        TableAttribute Table { get; }
        MappedProperty[] Columns { get; }
        MappedProperty SerialColumn { get; }
        bool HasSerialColumn { get; }
        string GetPrimaryKeyConstraintName();
        string GetSerialSequenceName();
    }

    /// <summary>
    /// Represents a type possibly marked with [Column] attributes and possibly strongly tied to a database
    /// table via the [Table] attribute.
    /// </summary>
    public class MappedType<T> : IMappedType
    {
        static Type actualType; // could be shadowed

        // the basics
        static TableAttribute table;
        static MappedProperty[] columns;
        static MappedProperty serialColumn;

        // cache for fast access
        static Dictionary<string, MappedProperty> columnMap;
        static Dictionary<string, MappedProperty> propertyMap;
        static Dictionary<string, MappedProperty> allProperties;

        // public info
        public TableAttribute Table { get { return table; } }
        public MappedProperty[] Columns { get { return columns; } }
        public MappedProperty SerialColumn { get { return serialColumn; } }
        public bool HasSerialColumn { get { return SerialColumn != null; } }

        static MappedType()
        {
            BuildTypeInformation();

            // if you didn't specify a [Table] attribute then we definitely can't shadow your type
            if (table == null) actualType = typeof(T);
        }

        static void BuildTypeInformation()
        {
            table = TableAttribute.Find(typeof(T)); // could be null if you only want to query

            // get a list of info objects regarding the properties of T marked with [Column]
            var list = new List<MappedProperty>();

            // also just collect all our properties in case you want to map one-way from postgres
            // to a type, and you don't want to bother with [Column] attributes
            allProperties = new Dictionary<string, MappedProperty>();

            BindingFlags propertyFlags = BindingFlags.Public | BindingFlags.Instance |
                BindingFlags.NonPublic;

            foreach (PropertyInfo property in typeof(T).GetProperties(propertyFlags))
            {
                allProperties[property.Name.ToLower()] = new MappedProperty(property, null, table);

                ColumnAttribute att = ColumnAttribute.Find(property);

                if (att != null)
                    list.Add(new MappedProperty(property, att, table));
            }

            BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.Instance;

            foreach (FieldInfo field in typeof(T).GetFields(fieldFlags))
            {
                allProperties[field.Name.ToLower()] = new MappedProperty(field, null);

                ColumnAttribute att = ColumnAttribute.Find(field);

                if (att != null)
                    list.Add(new MappedProperty(field, att));
            }

            if (list.FindAll(c => c.Column.IsSerial).Count > 1)
                throw new Exception("You can only mark one [Column] with IsSerial=true.");

            // slice and dice to optimize access later
            serialColumn = list.Find(c => c.Column.IsSerial);

            propertyMap = new Dictionary<string, MappedProperty>();
            columnMap = new Dictionary<string, MappedProperty>();

            list.ForEach(c => propertyMap[((MemberInfo)c.Property ?? c.Field).Name] = c);
            list.ForEach(c => columnMap[c.Column.Name] = c);

            columns = list.ToArray();
        }

        /// <summary>
        /// Gets an array of values of the given object's mapped properties (with [Column])
        /// matching the property names provided. The values will be adjusted to suit Postgres.
        /// </summary>
        public object[] GetColumnValues(T obj, IEnumerable<string> propertyNames)
        {
            var values = new List<object>();

            foreach (string propertyName in propertyNames)
                values.Add(propertyMap[propertyName].Get(obj));

            return values.ToArray();
        }

        /// <summary>
        /// Sets the [Column] mapped properties of the given object using raw columns and values
        /// from the given database row.
        /// </summary>
        public void SetProperties(PostgresRow row, T obj)
        {
            foreach (var pair in row)
            {
                if (columnMap.ContainsKey(pair.Key))
                {
                    MappedProperty prop = columnMap[pair.Key];
                    prop.Set(obj, pair.Value);
                }
                else
                {
                    // attempt to locate a property with a similar name, remove any underscores
                    string name = pair.Key.Replace("_","");

                    if (!allProperties.ContainsKey(name))
                    {
                        // maybe this is a Reference<> property named with the ID suffix
                        if (pair.Key.EndsWith("_id"))
                            name = pair.Key.Substring(0, pair.Key.Length - 3).Replace("_","");
                    }

                    if (allProperties.ContainsKey(name))
                    {
                        MappedProperty prop = allProperties[name];
                        prop.Set(obj, pair.Value);
                    }
                    //else throw new Exception("Could not find a property on " + typeof(T) + " that resembled the column name " + pair.Key);
                }
            }
        }

        /// <summary>
        /// Creates a new instance of our mapped type T, allowing for non-public constructors.
        /// </summary>
        public T CreateInstance(PostgresDatabase db)
        {
            // do we need to generate a dynamic type subclassing T? we've deferred it until now
            // when it's absolutely necessary. make sure to lock the type to avoid thread collisions.
            lock (typeof(T))
                if (actualType == null && table != null)
                    actualType = TrackedCreator.CreateTracked<T>(this);

            T obj = (T)Activator.CreateInstance(actualType, true); // allow nonpublic constructors

            ITrackChanges objWithChanges = obj as ITrackChanges;

            if (objWithChanges != null)
            {
                objWithChanges.Tracker.DB = db; // remember where it came from
                objWithChanges.Tracker.ClearChanges(); // in case your constructor set any defaults
            }

            return obj;
        }

        /// <summary>
        /// Creates a new instance of our mapped type T, initialized with the given database values,
        /// and allowing for a non-public constructor on T.
        /// </summary>
        public T CreateInstance(PostgresDatabase db, PostgresRow row)
        {
            T obj = CreateInstance(db);
            SetProperties(row, obj);

            ITrackChanges objWithChanges = obj as ITrackChanges;

            if (objWithChanges != null)
            {
                objWithChanges.Tracker.ClearChanges();
                objWithChanges.Tracker.ExistsInDatabase = true;
            }

            return obj;
        }

        public string GetSerialSequenceName()
        {
            // this is the formula Postgres uses to create sequences from the SERIAL keyword
            return string.Format("{0}_{1}_seq", Table.Name, SerialColumn.Column.Name);
        }

        public string GetPrimaryKeyConstraintName()
        {
            // this is the formula Postgres uses to create constraints from the PRIMARY KEY keyword
            return string.Format("{0}_pkey", Table.Name);
        }

        public string GetColumnName(string propertyName)
        {
            return propertyMap[propertyName].Column.Name;
        }
    }

    static class MappedTypeExtensions
    {
        public static bool IsMappedType(this Type type)
        {
            return type.BaseType.IsGenericType && 
                typeof(Mapped<>).IsAssignableFrom(type.BaseType.GetGenericTypeDefinition());
        }

        public static IMappedType GetMappedType(this Type type)
        {
            // field is different for every type subclass
            FieldInfo mappingField = type.BaseType.GetField("mapping",
                BindingFlags.NonPublic | BindingFlags.Static);

            return mappingField.GetValue(null) as IMappedType;
        }
    }
}
